Add the a8c-integration CLI (init + validate)#1
Conversation
3060680 to
402b034
Compare
Rule 2: drop no-op commands before following npm delegations, so an `echo npm test` can no longer expand into a real e2e run and smuggle a passing verdict past the filter. Rule 8: match the same e2e runner set as Rule 2 so docs using Cypress/Codeception/Puppeteer are not dinged.
Rule 3 only checked that the composer "validate-integration" key exists, so a stub like "echo ok" passed clean. Resolve the script body and drop no-op commands, the same guard Rule 2 uses, so an empty stub can no longer satisfy the gate. Asserting the real VIP validator runs is left until VIP publishes it (today it is a documented placeholder).
realCommands treated an entry starting with echo/:/true/# as a pure no-op, so a banner like `echo "Running" && phpunit` discarded the whole chain and Rule 2/3 saw no real command — a false FAIL that blocks partners who banner their test runs. Split each entry on shell separators first and drop only the segments that are themselves no-ops.
pandah3
left a comment
There was a problem hiding this comment.
Thank you for the huge effort porting this over!!
I got an error running the init command in the testing steps and needed to build it first:
a8c-integration init
zsh: command not found: a8c-integration
pnpm build
node bin/a8c-integration init
node bin/a8c-integration validate ./test-integration
Worked as expected!! But I haven't tweaked the integration to try to have it fail yet.
| program | ||
| .command( 'init' ) | ||
| .description( 'Start a new integration by scaffolding from the VIP Integrations Starter Kit.' ) | ||
| .option( '--vendor <vendor>', 'Vendor name (e.g. "Wordpress").' ) |
There was a problem hiding this comment.
| .option( '--vendor <vendor>', 'Vendor name (e.g. "Wordpress").' ) | |
| .option( '--vendor <vendor>', 'Vendor name (e.g. "WordPress").' ) |
| Interactive — it asks for your **vendor name** and **integration name**, always builds from the canonical [VIP Integrations Starter Kit](https://github.com/Automattic/vip-integrations-starter-kit) (its default branch, no git history pulled), rewrites the example prefix set to your names, renames the entry file, and starts a fresh git history. You can also pass the answers as flags: | ||
|
|
||
| ```bash | ||
| a8c-integration init --vendor "Wordpress" --name "Content Sync" |
There was a problem hiding this comment.
| a8c-integration init --vendor "Wordpress" --name "Content Sync" | |
| a8c-integration init --vendor "WordPress" --name "Content Sync" |
| // changelog or prose. Scanning arbitrary docs lets coincidental substrings | ||
| // pass, so only workflows and an explicit exception note count here. | ||
| if ( | ||
| /compatibility exception|approved exception/i.test( |
There was a problem hiding this comment.
Looks like this will match any record of compatibility exception or approved exception anywhere in docs/workflows, so if there's a line in the README that says, "There is no approved exception on file," it'll pass.
Should we gate this on something more explicit? Suggestions:
Option A: An HTML comment in the docs
<!-- vip:compatibility-exception approved -->
An HTML comment is handy here because markdown doesn't render it — it's invisible in the displayed README but sits in the source, so it reads as a deliberate machine marker, not prose. The check becomes anchored and specific:
const EXCEPTION_MARKER = /<!--\s*vip:compatibility-exception\s+approved\s*-->/i;
if (EXCEPTION_MARKER.test(ctx.docsText)) {
return { ...base, status: 'pass', message: '…' };
}
Option B: A structured field in composer.json (preferred)
{
"extra": {
"vip": { "compatibility-exception": "approved" }
}
}
const flag = ctx.composer?.extra?.vip?.['compatibility-exception'];
if (flag === 'approved') { … pass … }
Also, instead of auto-passing for this rule, should we require it to still route to the human-review section for sign-off or maybe downgrade to a warning that says "exception claimed, needs reviewer confirmation?"
| run: | | ||
| set -euo pipefail | ||
| tag_name="v$PACKAGE_VERSION" | ||
| gh release create "$tag_name" --target "$GITHUB_SHA" --title "$tag_name" --generate-notes |
There was a problem hiding this comment.
This checks out ref: trunk up top, so it always builds and publishes trunk's code — but the release is created with --target "$GITHUB_SHA". On a workflow_dispatch, GITHUB_SHA is the commit of whatever ref you triggered from, which the UI lets you pick. So if someone dispatches from a feature branch, we publish trunk to npm but tag vX.Y.Z on the feature branch commit. The tag ends up pointing at code that was never shipped.
Should we tag the tree we actually built? --target "$(git rev-parse HEAD)" after checkout would keep the tag and the published code in sync.
|
|
||
| function checkGracefulConfigHandling( ctx: Context ): CheckResult { | ||
| const base = { | ||
| id: 'graceful-config-handling', |
There was a problem hiding this comment.
The config guard (rule 5) passes if is_array( / is_ready( / is_available( appears anywhere in the concatenated PHP source, and rule 9's telemetry guard does the same with class_exists(. Those are generic functions used all over a normal plugin, so an unrelated is_array() in some helper makes it print "Config access is guarded against a missing or invalid constant" even when the actual config read has no guard at all. It's checking "does this word exist in the codebase," not "is the risky call guarded."
Could we scope the search to a window around the config constant (and around record_event( for rule 9)? Same approach the secret-key scan just below already uses.
| if ( blocks.length < 2 ) { | ||
| return { | ||
| ...base, | ||
| status: 'fail', |
There was a problem hiding this comment.
Rule 6 tells "valid" from "incomplete" by counting keys. If one doc example has fewer keys than another, it calls that pair valid+incomplete. But fewer keys usually just means fewer optional settings, not a missing required field, so two perfectly valid examples of different lengths pass as if one were the broken case the rule is trying to require.
Detecting "missing a required field" statically is genuinely hard, so maybe we lean on the explicit signal instead. The mentionsIncomplete regex is already there. Require the docs to actually label the incomplete example rather than guessing from key count?
| // cloned plainly (git ignores --depth for local paths and warns). | ||
| const depth = isRemote ? [ '--depth', '1' ] : []; | ||
| // `--` ends option parsing so the source can never be read as a git flag. | ||
| execFileSync( 'git', [ 'clone', ...depth, '--quiet', '--', source, target ], { |
There was a problem hiding this comment.
If the clone dies partway (network drop) or scaffoldTree throws, we leave a half-populated target on disk. The next init then hits the "Target directory is not empty" guard and the user is stuck cleaning up by hand and the error doesn't say why. The comment at line 86 says we fail fast so we don't leave a half-laid-down directory behind, but that only covers the name validation, not these two spots.
Can we wrap the clone + scaffold in a try/catch and rmSync(target, { recursive: true, force: true }) on failure, so a failed run leaves nothing behind?
| console.log( gray( `Laying down the VIP Starter Kit into ${ target }` ) ); | ||
| laySkeleton( target ); | ||
|
|
||
| const { entryFile, prefix } = scaffoldTree( target, vendor, name ); |
There was a problem hiding this comment.
| /** Paths (relative, forward-slash) that must not be rewritten. Mirrors the | ||
| * skip set in bin/setup.php: dependencies, lockfiles, the scaffolder itself, | ||
| * and binary assets. */ | ||
| const SKIP_PATTERN = |
There was a problem hiding this comment.
The skip set only excludes .png/.jpg/.jpeg/.gif/.webp, but we read every other file as utf8 and write it back. If the kit ships any other binary (.ico, .woff2, .ttf, .pdf) that happens to contain a token's ASCII bytes, reading it as utf8 swaps the invalid bytes for U+FFFD and we write the corrupted version back. The updated !== contents check saves token-free binaries, so it only bites a binary that also contains a token — but the comment says we skip "binary assets" and we only skip some of them.
Could we either broaden the skip list to the other binary types, or sniff for a NUL byte and skip anything that looks binary before rewriting?
| missing.push( 'WordPress 7.0' ); | ||
| } | ||
| for ( const php of [ '8.2', '8.3', '8.4', '8.5' ] ) { | ||
| if ( ! ctx.workflowsText.includes( php ) ) { |
There was a problem hiding this comment.
The PHP versions are checked with .includes('8.4'), so it matches that string anywhere in the workflow YAML, not just on a PHP-version line. CI files are full of other version numbers — node-version: 18.4, mysql:8.4, a pinned action tag — any of those makes it think PHP 8.4 is covered. So an integration that only tests 8.2 but has a Node matrix of 18.3/18.4/18.5 reports full 8.2–8.5 coverage and passes.
The WP checks right above already dodge this with \b6.9\b. A word boundary here helps (\b8.4\b won't match inside 18.4), but it still matches mysql:8.4, so ideally we scope the match to a php-version key the way the wpLatest pattern scopes to a WP key. Can we tie it to the php line instead of a loose substring?
Drop Rule 3's `composer run validate-integration` check — that validation is the CLI's own job now, so requiring the integration to re-declare it was circular. In its place, Rule 3 validates the handoff manifest (vip-handoff.yaml): the single file a partner fills in so VIP can register and load the integration from the manifest alone. The new manifest.ts parses the YAML and checks every field VIP consumes (identity, plugin runtime, runtime-config schema) is present and well-formed. Also harden the existing rules per review: - Rule 2 matches a test runner as the invoked command, not a substring, so `rm -rf cypress-artifacts` no longer counts as an e2e run. - Rules 5 and 9 scope their guard search to the neighbourhood of the config constant / telemetry call instead of the whole source. - Rule 6 requires the incomplete config example to be labeled instead of guessing from key counts. - Rule 7 gates the compatibility exception on a structured composer.json flag (extra.vip.compatibility-exception) and warns rather than auto-passing, and scopes the PHP-version check to php keys so mysql:8.4 or a node matrix can't be read as PHP coverage.
Wrap the clone and scaffold in a try/catch so a clone that dies partway or a scaffold that throws no longer leaves a half-populated directory behind — which would otherwise trip the "not empty" guard on the next run. Restore the empty directory if the user had created it themselves. Also use "WordPress" (correct casing) in the vendor prompt example.
The rewrite only skipped a handful of image extensions, so any other binary the kit ships (.ico, .woff2, .ttf, .pdf) was read as utf8 and written back — corrupting its non-token bytes into U+FFFD. Sniff for a NUL byte, the reliable binary tell, and skip anything binary.
The job checks out and builds trunk but tagged the release with $GITHUB_SHA, which on a manual dispatch is the commit the run was started from — a feature branch — so the tag could point at code that was never published. Tag trunk's tip (the tree we actually built).
Fix the casing in the docs and CLI help examples.
|
@pandah3 I added another role: "Handoff manifest is present and complete". This will check the manifest file to see if it contains the fields that VIP needs to build our side of things. |
Description
a8c-integrationis a standalone CLI for building WordPress VIP Integration Center add-ons. It exists so partners outside Automattic can scaffold an integration and check it for conformance locally and in CI. It ships two commands:init(scaffold a new integration) andvalidate(conformance-check one).What this PR adds
initcommand — scaffolds from the VIP Integrations Starter Kit: clones it (no git history), rewrites the example prefix set to the partner's vendor/name, renames the entry file, starts a fresh repo.validatecommand — the conformance checker with human and--format jsonoutput; exits1when non-conformant.README.md,SETUP.md,ARCHITECTURE.md,TESTING.md,RELEASING.md.lint.yml(for eslint, prettier, types, build) andtest.yml(jest on Node 20 & 22).npm-publish.ymlfor the Github Actions that does the npm publishing.Tech stack
tsc;bin/a8c-integrationis a thin launcher.@automattic/eslint-plugin-wpvip), and wp-prettier.How to test
init— scaffold a new integration:validate— check the scaffold (exit code0):